package cn.cosmosx.base.util.check;

import org.apache.commons.lang3.StringUtils;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Pattern;

/**
 * 身份证号校验工具类
 * 仅支持15位或18位身份证号
 *
 * @author gengzhy
 * @date 2022-9-22
 */
public final class IDCardCheckUtil {

    /**
     * 身份证号码合法性校验（仅支持15位或18位身份证号）
     * @param idNumber 身份证号参数
     * @return True：合法；False：非法
     */
    public static boolean verify(String idNumber) {
        boolean isLegal;
        switch (idNumber.length()) {
            case Constant.ID_NUM_LENGTH_18:
                // 18为身份证号校验
                isLegal = verifyIDNumber18(idNumber);
                break;
            case Constant.ID_NUM_LENGTH_15:
                // 15为身份证号校验
                isLegal = verifyIDNumber15(idNumber);
                break;
            default:
                isLegal = false;
                break;
        }
        return isLegal;
    }

    /**
     * 18位身份证号合法性校验
     * @param src 身份证号
     * @return True：合法；False：非法
     */
    public static boolean verifyIDNumber18(String src){
        // 校验长度
        if (StringUtils.isBlank(src) || src.length() != Constant.ID_NUM_LENGTH_18) {
            return false;
        }
        // 前17位数字判断
        String before17 = src.substring(0, 17);
        if (!Constant.isNumber(before17)) {
            return false;
        }
        // 判断前两位是否为身份编码
        if (Constant.PROVINCE_CODE.get(src.substring(0, 2)) == null) {
            return false;
        }
        // 出生年月日校验
        if (!Constant.checkBirthday(src.substring(6, 14))) {
            return false;
        }
        // 根据前17为数字计算出真实校验码，与最后一位校验码做匹配
        String validateCode = Constant.calcValidateCode(before17.toCharArray());
        return StringUtils.equals(src.substring(before17.length()), validateCode);
    }

    /**
     * 15位身份证号合法性校验
     * @param src 身份证号
     * @return True：合法；False：非法
     */
    public static boolean verifyIDNumber15(String src){
        if (StringUtils.isBlank(src) || src.length() != Constant.ID_NUM_LENGTH_15 || !Constant.isNumber(src)) {
            return false;
        }
        // 前两位省份判断
        if (Constant.PROVINCE_CODE.get(src.substring(0, 2)) == null) {
            return false;
        }
        // 出生年月日校验
        return Constant.checkBirthday(src.substring(6, 12));
    }

    /**
     * 身份证号码校验辅助工具类
     */
    private static class Constant {
        /**长度为15位的身份证号码*/
        private final static int ID_NUM_LENGTH_15 = 15;
        /**长度为18位的身份证号码*/
        private final static int ID_NUM_LENGTH_18 = 18;
        /**支持的最早年份*/
        private final static int MIN_YEAR = 1930;
        /** 省、直辖市代码表 */
        final static Map<String, String> PROVINCE_CODE = new HashMap<>();
        /**最小月份*/
        private final static int MIN_MONTH = 1;
        /**最大月份*/
        private final static int MAX_MONTH = 12;
        /**正整数正则*/
        private final static Pattern NUMBER_PATTERN = Pattern.compile("^\\d+$");
        /** 身份证号第1位到第17位对应的加权因子 */
        static final int[] POWER = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};
        /** 身份证前17位的各位加权之和与11的取余 与 身份证号第18位校验码 的对应关系 */
        static final Map<Integer, String> VERIFY_CODE = new HashMap<>();

        static {
            /*缓存 省、直辖市代码表*/
            PROVINCE_CODE.put("11", "北京");
            PROVINCE_CODE.put("12", "天津");
            PROVINCE_CODE.put("13", "河北");
            PROVINCE_CODE.put("14", "山西");
            PROVINCE_CODE.put("15", "内蒙古");
            PROVINCE_CODE.put("21", "辽宁");
            PROVINCE_CODE.put("22", "吉林");
            PROVINCE_CODE.put("23", "黑龙江");
            PROVINCE_CODE.put("31", "上海");
            PROVINCE_CODE.put("32", "江苏");
            PROVINCE_CODE.put("33", "浙江");
            PROVINCE_CODE.put("34", "安徽");
            PROVINCE_CODE.put("35", "福建");
            PROVINCE_CODE.put("36", "江西");
            PROVINCE_CODE.put("37", "山东");
            PROVINCE_CODE.put("41", "河南");
            PROVINCE_CODE.put("42", "湖北");
            PROVINCE_CODE.put("43", "湖南");
            PROVINCE_CODE.put("44", "广东");
            PROVINCE_CODE.put("45", "广西");
            PROVINCE_CODE.put("46", "海南");
            PROVINCE_CODE.put("50", "重庆");
            PROVINCE_CODE.put("51", "四川");
            PROVINCE_CODE.put("52", "贵州");
            PROVINCE_CODE.put("53", "云南");
            PROVINCE_CODE.put("54", "西藏");
            PROVINCE_CODE.put("61", "陕西");
            PROVINCE_CODE.put("62", "甘肃");
            PROVINCE_CODE.put("63", "青海");
            PROVINCE_CODE.put("64", "宁夏");
            PROVINCE_CODE.put("65", "新疆");
            PROVINCE_CODE.put("71", "台湾");
            PROVINCE_CODE.put("81", "香港");
            PROVINCE_CODE.put("82", "澳门");
            PROVINCE_CODE.put("91", "国外");

            /*18位身份证号，前17位权重与其数字对应map*/
            VERIFY_CODE.put(0, "1");
            VERIFY_CODE.put(1, "0");
            VERIFY_CODE.put(2, "X");
            VERIFY_CODE.put(3, "9");
            VERIFY_CODE.put(4, "8");
            VERIFY_CODE.put(5, "7");
            VERIFY_CODE.put(6, "6");
            VERIFY_CODE.put(7, "5");
            VERIFY_CODE.put(8, "4");
            VERIFY_CODE.put(9, "3");
            VERIFY_CODE.put(10, "2");
        }

        /**
         * 出生年月日校验（yyyyMMdd 或 yyMMdd）
         * @return True：合法；False：不合法
         */
        static boolean checkBirthday(String birthCode) {
            int month = 0;
            int day = 0;
            Date birthDate = null;
            try {
                if (birthCode.length() == 8) {
                    month = Integer.parseInt(birthCode.substring(4, 6));
                    day = Integer.parseInt(birthCode.substring(6, 8));
                    birthDate = new SimpleDateFormat("yyyyMMdd").parse(birthCode);
                } else if (birthCode.length() == 6) {
                    month = Integer.parseInt(birthCode.substring(2, 4));
                    day = Integer.parseInt(birthCode.substring(4, 6));
                    birthDate = new SimpleDateFormat("yyMMdd").parse(birthCode);
                } else {
                    throw new IllegalArgumentException("不支持该日期格式");
                }
            } catch (ParseException ignored) {}
            Calendar cal = Calendar.getInstance();
            if (birthDate != null){
                cal.setTime(birthDate);
            }
            return validateDate(cal.get(Calendar.YEAR), month, day);
        }

        /**
         * 校验年月日是否合法
         * @param year
         * @param month
         * @param day
         * @return
         */
        static boolean validateDate(int year, int month, int day) {
            Calendar cal = Calendar.getInstance();
            int y = cal.get(Calendar.YEAR);
            int datePerMonth;
            if (year < MIN_YEAR || year >= y) {
                return false;
            }
            if (month < MIN_MONTH || month > MAX_MONTH) {
                return false;
            }
            switch (month) {
                case 4:
                case 6:
                case 9:
                case 11:
                    datePerMonth = 30;
                    break;
                case 2:
                    datePerMonth = new GregorianCalendar().isLeapYear(year) ? 29 : 28;
                    break;
                default:
                    datePerMonth = 31;
            }
            return (day >= 1 && day <= datePerMonth);
        }

        /**
         * @Description 根据前身份证号17位加权计算得到校验码
         * @return
         */
        static String calcValidateCode(char[] charArr){
            // 前17位加权因子
            int[] powerArr = Constant.POWER;
            // 权重值和
            int sum = 0;
            for (int i = 0; i < charArr.length; i++) {
                sum += Character.getNumericValue(charArr[i]) * powerArr[i];
            }
            int mod = sum % 11;
            return Constant.VERIFY_CODE.get(mod);
        }

        /**
         * 校验是否是正整数
         * @param source
         * @return
         */
        static boolean isNumber(String source) {
            return source == null ? false : NUMBER_PATTERN.matcher(source).matches();
        }
    }
}
